查看原文
其他

C++之多线程(二)

思想觉悟 思想觉悟 2022-10-09

导读

做开发的人都知道多线程是一个很复杂的问题,一不下心就会出现莫名其妙的八哥,有句话调侃说:

一个程序员碰到了一个问题,他决定用多线程来解决。现在他有了两个问题。。。

在前面《C++之多线程(一)》 一文中,我们介绍了C++11中多线程的一些基本使用以及给线程传递参数时的一些注意事项。今天我们继续了解下C++11中多线程一些比较现代化的用法,以及一些线程同步的方法。

async、future

设想一下现在我们有这样的一个需求,我们需要开启一个线程去某件事情,待这件事情执行完毕后将结果返回,那该如何实现呢?当然我们可以可以通过全局变量或者给线程传参的方式来实现这个需求,但是否有更简单灵活的方式呢?

furure的中文意思是将来,它表示在未来的等待线程执行完毕后可以获取线程返回的结果值。

async是一个函数模版,通过它可以启动一个异步任务,并且返回一个future,通过这个future可以获取到异步任务的执行结果。

以下是一个简单的例子:

#include <future>
int request(){
    std::cout << "request线程:" << std::this_thread::get_id() << std::endl;
    this_thread::sleep_for(std::chrono::seconds(3)); // 休眠3s
    return 20;
}

int main() {
    std::future<int> future = async(request);
    int result = future.get(); // 阻塞,并且只能get一次
    std::cout << "当前线程:" << std::this_thread::get_id() << std::endl;
    std::cout << "结果的值:" << result << std::endl;
    return 0;
}

正如上述代码注释所写的那样,std::future仅仅允许get一次结果值,如果希望多次获取结果值怎么办?使用shared_future代替std::future即可。

不同于thread一创建就马上执行线程函数,async是可以通过launch参数控制什么时候执行的,单单是这个就比thread`灵活了。

launch参数有三种可选的值,分别是async=1deferred=2any = async | deferred

1、async 是默认行为,表示马上创新新线程执行 2、deferred 延迟执行,需要等待future的get或await才自行线程任务,不一定会会创建新线程 3、any    是前面两个的结合体,一定会创建新线程,但是也是需要等待future的get或await才自行线程任务

其他的比如给async传递参数时应该用什么传递呢?值传递、指针传递、引用传递怎么选呢?童鞋们可以按照前面的文章《C++之多线程(一)》 自行编码验证。

互斥量与条件变量

1、互斥量

互斥量与条件变量相信大家都不陌生了,一扯到线程同步,线程死锁啥的必然逃不开这两家伙...

std::mutex是C++11引入的关于互斥量最基本的类,同时还附带了一系列RAII语法的模板类,例如std::lock_guard。RAII 在不失代码简 洁性的同时,很好地保证了代码的异常安全性。为了保证代码安全,避免忘记释放而导致的死锁问题等,我们一般不直接使用std::mutexlock函数,而是使用unique_lock或者std::lock_guard等。

首先说说std::lock_guard,它是内部同时帮我们做了std::mutexlockunlock操作,也就是说使用了std::lock_guard就完全释放了,不用再手动地去调用lockunlock了。

以下是一个使用std::lock_guard保证线程安全的例子:

int main() {
    std::mutex mutex;
    int a = 0;
    thread thread1([&](){
        for (int i = 0; i < 100000; ++i) {
            std::lock_guard<std::mutex> lockGuard(mutex);
            a++;
        }
    });
    thread thread2([&](){
        for (int i = 0; i < 100000; ++i) {
            std::lock_guard<std::mutex> lockGuard(mutex);
            a++;
        }
    });
    thread1.join();
    thread2.join();
    std::cout << "a的值:" << a << std::endl;
    return 0;
}

通过使用互斥量,我们保证了变量a的值每次只允许一个线程进行修改,所以保证了运作完毕后a的值就是我们所期待的结果。

std::unique_lock的功能和std::lock_guard功能类似,但是比std::lock_guard更为灵活,可配性更高。

2、条件变量

条件变量就类似于一个跑腿的通信工,当条件满足的时候它会通知某线程继续执行任务,当条件不满足的时候它就通知某线程进入等待休息。

C++11中提供了std::condition_variable作为条件变量。下面我们使用std::condition_variable来模拟一个典型的消费者生产者的线程同步的例子:

int main() {
    std::mutex mutex;
    std::condition_variable conditionVariable;
    std::vector<int> task_queue;
    // 生产者线程
    thread product_thread([&](){
        while (true){
            std::this_thread::sleep_for(std::chrono::milliseconds (600));
            task_queue.emplace_back(1);
            std::cout << "生产成功一个产品" << std::endl;
            // 通知唤醒
            conditionVariable.notify_all();
        }
    });
    // 消费者线程
    thread consume_thread([&](){
        while (true){
            std::unique_lock<std::mutex> uniqueLock(mutex);
            // 防止虚假唤醒
            if(task_queue.empty()){
                std::cout << "等待生产者生产" << std::endl;
                conditionVariable.wait(uniqueLock);
            } else{
                std::cout << "取出消费" << std::endl;
                task_queue.erase(task_queue.cbegin());// 移除第0个
                std::this_thread::sleep_for(std::chrono::milliseconds (500));
            }
            uniqueLock.unlock();
        }
    });
    product_thread.join();
    consume_thread.join();
    return 0;
}

上面的注释有一个虚假唤醒的判断,所谓的虚假唤醒意思就是:当一个正在等待条件变量的线程由于条件变量被触发而唤醒时,却发现它等待的条件没有满足就被唤醒了,比如在条件其实没有满足时却调用了notify_all去通知。

原子操作

上面我们讲述了互斥量与条件变量的相关操作,虽然它们搭配使用做到了线程安全,但是仔细想想,有时候我们仅仅是需要在不同的线程读写一个简单的数据而已,也需要动用到互斥量与条件变量吗?

杀鸡焉用牛刀,这不是平A骗大招吗?

大家平时在了解多线程概念的时候都知道,即使是一条简单的赋值语言,相对于CPU而言,也是需要拆分成多个步骤才能完成的,而CPU一般又是抢占式的,那么在这个拆分到修改完成的这个过程中如果原有变量的再次被修改了那么矛盾就产生了, 如果需要避免这种矛盾的产生就需要变量的修改应该是原子操作。

在C++11中引入了std::atomic来代表原子操作,我们通过以下程序来看看原子操作的魅力:

int main() {
    int a = 0;
    thread thread1([&](){
        for (int i = 0; i < 100000; ++i) {
            a++;
        }
    });
    thread thread2([&](){
        for (int i = 0; i < 100000; ++i) {
            a++;
        }
    });
    std::atomic<int> b{0};
    thread thread3([&](){
        for (int i = 0; i < 100000; ++i) {
            b++;
        }
    });
    thread thread4([&](){
        for (int i = 0; i < 100000; ++i) {
            b++;
        }
    });
    thread1.join();
    thread2.join();
    thread3.join();
    thread4.join();
    std::cout << "a的值:" << a << std::endl;
    std::cout << "b的值:" << b << std::endl;
    return 0;
}

在上面的程序中,变量a和变量b都使用了多线程进行修改值,但是最后的结果只有变量b的值被正确地修改为200000,而a的随着每次运行都是不确定,不准确的,因为对变量a修改时不是原子操作。

注意,虽然atomic表示原子操作,但是也并不意味着对atomic变量进行的所有表达式操作都是原子,在使用atomic的过程最好使用atomic内的函数进行操作,不要像笔者的例子那样直接进行加加渐渐,比如想要增加就调用fetch_add函数等。

推荐阅读

C++之指针扫盲
C++之智能指针
C++之指针与引用
C++之右值引用C++之多线程一

关注我,一起进步,人生不止coding!!!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存